Build 450 v0.9.7

---

## Kinky traits: remove unintended auto-assign on Sim instantiation

**Tracker:** `TODOs (451)/Kinky Traits NPC assignment` — **shipped** (remove hook, not gate). **Separate** from spread orientation filter (next section).

### Background (player-visible symptom)

- Fresh **CAS** Sims with **zero** Kinky Traits could receive **~3 Kinky Traits** (e.g. Dominant + Big Cock Lover + Elder Lover, or Bald Pussy Lover + Big Butt Lover + Pervert) after enabling KW or opening the **Skills** tab — **without** using the trait picker or **Spread Kinky Traits**.
- **Five vanilla CAS traits** remained intact (not legacy CAS→Skills conversion).
- Reproduced on **brand-new worlds**, new households, and **Edit Town household switch**; **reporter confirmed on Build 450** (KW-only mod).
- Same Sim always received the **same three traits** (deterministic per `SimDescription`, not a random roll).

### What this bug was not

| Misread | Actual |
|---------|--------|
| Legacy CAS → unified Skills migration | Vanilla CAS slots stayed at 5; traits appeared in Kinky list only |
| **Spread Kinky Traits** running silently | Spread is manual (Global → Kinky Traits) + one-time init flag |
| Trait picker auto-opening | Picker still requires Accept/Cancel dialog |

### Root cause (technical)

- Unintended hook on **`kSimInstantiated`** in **`Main.OnSimInstantiated`** (Build 450 and earlier):

```csharp
if (!sim.IsSelectable)
    KinkyTraitsState.EnsureNpcBaselineTraits(sim.SimDescription);
```

- **`EnsureNpcBaselineTraits`** → private **`EnsureBaselineTraits`** (target **1–2 Core + 2 Preference**, typically **3** visible traits).
- **`IsSelectable`** is **not reliable** at instantiate time (Edit Town switch, lot placement, load transitions): **active-household playable Sims** could be `!IsSelectable` transiently → baseline written to **`SimData`**.
- Hook did **not** check **`Settings.Enabled`** — traits could persist even before the player enabled KW in UI.
- **Not** part of shipped spread design (Build 446 changelog: manual spread only); residual from wave 445–446 Option B, never in official roadmap.

### Fix (code verified — active `Project` tree)

| Area | File | Behavior |
|------|------|----------|
| Remove instantiate hook | `Oniki/Main.cs` | **`OnSimInstantiated`** no longer calls trait baseline; listener retained for other human setup (autonomy, injections, zoo penis, etc.). |
| Remove dead API | `Oniki.Gameplay/KinkyTraitsState.cs` | Public **`EnsureNpcBaselineTraits`** removed; grep shows **no** remaining references in project sources. |
| Spread-only baseline | same | Private **`EnsureBaselineTraits`** kept — invoked only from **`EnsureKinkyTraitsSpreadState`** / manual **Spread Kinky Traits** pass. |

**Product decision:** remove entirely — **no** new toggle (e.g. `AssignKinkyTraitsToNewNpcs` rejected).

### Valid assignment channels (post-fix policy)

| Channel | How |
|---------|-----|
| **Manual picker** | Selectable Sim — Skills tab / pie menu (Accept + editor) |
| **Spread Kinky Traits** | Global → Kinky Traits Settings (+ sample %) |
| **Gameplay rewards** | WooHooer, Cheater, whore career, trait earned paths, etc. |
| **Legacy migration** | CAS → unified on load / **`MigrateAllSimsToUnifiedTraits`** |

**Excluded:** any auto-assign on **`kSimInstantiated`** or live NPC/household spawn.

### Unchanged (explicit)

- **Manual trait picker**, **Spread Kinky Traits** + **`KinkyTraitsSpreadSamplePercent`**, reward paths, legacy migration — all as before.
- **No retro-delete** on saves that already received erroneous baseline from Build 450.

### Historical note

Build **446** release intent documented *no `OnSimInstantiated` hooks* for trait spread; the NPC baseline hook was leftover incomplete work mistakenly inserted in previous builds. Build **451** aligns runtime with that intent.

### Player support note (non-technical, for release reply)

- **450 → 0.9.7:** KW no longer auto-assigns a starter set of Kinky Traits when Sims spawn or when you enable the mod. Use the **trait picker** or **Spread Kinky Traits** if you want NPCs/world populated. If unwanted traits were already saved on a Sim from 450, open the picker and adjust them manually.

### Diagnostics

- **No new STBL** in this patch.

---

## Kinky traits spread: orientation-aware preference filter (exclusive gay / lesbian)

### Background (player-visible symptom)

- Community request: Sims who identify as **exclusively gay** (male) or **exclusively lesbian** (female) could receive **hetero-coded preference traits** from the **Spread Kinky Traits** pass — e.g. a gay man with **Love Big Boobs**, **Love Bald Pussy**, or **Love Hairy**; a lesbian with **Love Big Cock**.
- Traits were not “wrong” mechanically (they still applied as body-preference modifiers) but felt incoherent with the Sim’s **EA gender preference** profile.
- Workaround before fix: remove traits manually via debug / trait picker.

### Root cause (technical)

- **`KinkyTraitsState.EnsureBaselineTraits`** → **`PickDeterministicTraitForTier`** assigns **1–2 Core + 2 Preference** traits to eligible **non-active-household** townies on the manual spread pass.
- Candidate pool for **Preference** tier included all migratable preference traits with only **conflict / pack / already-owned** filters — **no check** on `SimDescription.mGenderPreferenceMale` / `mGenderPreferenceFemale` or Sim gender.
- KW already exposes orientation helpers elsewhere (`SimTools.IsHomo`, `IsLesbian`, etc.) but spread did not use them.
- **Bisexual** Sims (`both preferences > 0`) must **not** be filtered — user-confirmed design.

### Fix

| Area | File | Behavior |
|------|------|----------|
| Exclusive-orientation checks | `Oniki.Gameplay/KinkyTraitsState.cs` | **`IsExclusiveHomosexualMale`**: `IsMale && mGenderPreferenceMale > 0 && mGenderPreferenceFemale < 0`. **`IsExclusiveLesbianFemale`**: `IsFemale && mGenderPreferenceFemale > 0 && mGenderPreferenceMale < 0`. |
| Preference pool filter | same — **`ShouldExcludePreferenceTraitForSpreadOrientation`** | Applied only when **`tier == Preference`** inside **`PickDeterministicTraitForTier`**. |
| Excluded traits (exclusive gay male) | same | **`LoveBigBoobs`**, **`LoveBaldPussy`**, **`LoveHairy`** (hairy/bald pair = female pubic preference; same logic as bald pussy). |
| Excluded traits (exclusive lesbian) | same | **`LoveBigCock`**. |
| Not excluded (v1) | same | **`LoveCurvy`**, **`LoveBigButt`**, **`LoveMuscular`**, **`LoveFeet`**, etc. — still valid for any orientation. |

### Orientation logic (reference)

| Profile | Detection | Spread exclusion |
|---------|-----------|------------------|
| Exclusive gay male | Male + male pref **>** 0 + female pref **<** 0 | Boobs / bald pussy / hairy |
| Exclusive lesbian | Female + female pref **>** 0 + male pref **<** 0 | Big cock |
| Bisexual | Both prefs **>** 0 | **None** (unchanged pool) |
| Straight | Opposite pref **>** 0, same-sex pref **<** 0 | **None** |
| Asexual | Both prefs **<** 0 | **None** |

Uses the same **`mGenderPreference*`** fields as **`Sim_ChangeGenderPreferences`** presets and **`SimTools`** orientation helpers — not a separate identity flag.

### Unchanged (explicit)

- **Spread eligibility** (non-active-household human townies, teen+, sample percent, one-time **`mKinkyTraitsSpreadInitialized`** gate).
- **Core trait** spread (Slut, Voyeur, Dominant, etc.) — **no** orientation filter.
- **Manual trait picker**, **reward paths**, **social trait discovery**, **debug assign**.
- Sims **already spread** before this patch: **no retroactive removal** (init flag already set).
- **No new settings toggle** — filter is always on for spread pass.

### Safety (spread robustness)

- If filtering empties the preference candidate list, **`PickDeterministicTraitForTier`** returns **`TraitNames.Unknown`** and **`EnsureBaselineTraits`** **breaks** the preference loop (pre-existing behavior) — **no crash**, Sim may receive **fewer than 2** preference traits (already possible via conflicts).

### Player support note (non-technical, for release reply)

- **Spread Kinky Traits** now skips a few **body-preference** traits that don’t match **exclusive** gay/lesbian orientation (based on the game’s gender-preference values). **Bisexual** Sims are unaffected. Traits already saved on Sims from an older spread are **not** auto-removed — use the trait picker or debug if you want to clean them up.

### Diagnostics

- **No new STBL** in this patch.
- Verify orientation via debug **Show Infos** (`Is Homo` / `Is Lesbian` + numeric gender prefs) before/after spread on test townies.

---

## Glory hole (bathroom stall): presenter undress + documented side-pick backlog

**Scope:** `ToiletStallWithGloryHole` only (`Oniki.Interactions/UseToilet.cs`, `UseGloryHole`, `JoinGloryHole`). **Not** the decorative **`HoleInWall`** object (fixed `_route_L` / `_route_R` — separate follow-up).

### Shipped in Build 451 — guaranteed lower-body undress (presenting Sim)

#### Background (player-visible symptom)

- On **ToiletStallWithGloryHole**, the Sim presenting through the hole could remain dressed on the lower body when the partner started handjob/oral/vaginal.
- **Erection** could be active internally, but **no penis visible** through the hole because pants/underwear were still on.
- Reported with direct **Use Glory Hole** / **Join Glory Hole** flows, not only autonomy.

#### Root cause (technical)

- Presenter visibility depends on **`NakedFlags.LowerNaked`** before `StartErection`: `OutfitTools.UpdateLayersAndErection` applies penis morph only when `HasNakedPart(LowerBody)`.
- Glory hole relied on generic **`UseToiletNaked`** at interaction start; that path is skipped when **`ForceUseGloryHole`** forces `mCanMasturbate = false` under **`UseToiletNaked = ArousedOnly`**, and there was **no dedicated undress** when entering **`UseGloryHole`** state.
- **`StartGloryHoleWooHoo`** sets **`WooHooInstance.ShouldChangeOutfits = false`**, so WooHoo outfit prep never compensates.

#### Fix (code verified — `UseToilet.cs`)

| Area | File | Behavior |
|------|------|----------|
| Presenter undress helper | `Oniki.Interactions/UseToilet.cs` | **`EnsureGloryHolePresenterUndressed()`**: male presenter → **`LowerNaked \| PantyLess`**, preserve pre-GH outfit via **`SwitchOutfitHelper`** when needed, then erection path. |
| Glory hole entry | same | Called when male enters **`UseGloryHole`** state (replaces bare **`StartErection`** only). |
| Partner join | same | Called in **`Join()`** on presenter stall when a partner attaches. |
| WooHoo start | same | Called from **`StartGloryHoleWooHoo()`** before **`WooHooInstance.Start()`**. |

#### Background (separate community report)

- Male Sim on bathroom GH stall may always present through the hole on the **same lateral side** (`IsMirrored` bias).
- **Rightmost stall** (wall to the right) or **isolated stall**: player **Use Glory Hole** can **enter and exit without using the hole** — silent **`PeeStanding`** then exit when **`TryGloryHoleMale`** returns false.

#### Root cause (technical — current code)

| Area | Behavior |
|------|----------|
| Side pick | **`TryGloryHoleMale(out isMirrored)`** scans adjacent stalls within **~1.1 tile** lateral offset; sets **`IsMirrored`** for animation flip only (does not rotate stall facing). |
| Switch gap | Under **`ForceUseGloryHole`**, `switch (num2)` succeeds for `case 1` (free left neighbor) and `case 3` + `num==1` (occupied left, free right); **no match** for capolinea patterns (`num2=3, num=0` or `num=1, num2=0` or no neighbors) → returns **false**. |
| Player fallback | `UseToilet.Run()` tail: `else if (ForceUseGloryHole)` → **`PeeStanding`** only, then **`Exit`** — no TNS / no retry (lines ~1162–1167 in current source). |

**Failure matrix (code-derived — in-game repro pending):**

| Stall position | Left neighbor | Right neighbor | `TryGloryHoleMale` (ForceUseGloryHole) |
|----------------|---------------|----------------|----------------------------------------|
| Middle | free | free | OK, `IsMirrored=false` (left preference) |
| Middle | occupied (no GH) | free | OK, `IsMirrored=true` |
| **Right end** | occupied (no GH) | wall | **FAIL** → pee + exit |
| **Right end** | none in range | wall | **FAIL** |
| **Left end** | wall | free | **FAIL** |
| Isolated | none | none | **FAIL** |

#### Unchanged in 451 (both topics)

- **`UseToiletNaked`** still controls generic toilet undress for non-glory-hole use.
- **`ShouldChangeOutfits = false`** on glory-hole **`WooHooInstance`**.
- Receiver-side stall / autonomy join paths — no side-pick rewrite.
- **`HoleInWall`** decor object — out of scope for bathroom stall backlog.

### Diagnostics

- **Shipped (undress):** no new logging toggles or STBL keys.
- **Backlog (side-pick):** optional future key `Oniki.KinkyMod.GloryHole:FeedbackSideUnavailable` if player feedback is added; wave 1 design allows silent fix without STBL.

---

## Toilet use: autonomous masturbation toggle + mid-act lower undress

**Tracker:** `TODOs (451)/Masturbate on Toilet` — **shipped** (code). STBL label pending (S3SE). **Separate** from glory-hole presenter undress (`EnsureGloryHolePresenterUndressed`) — same file, different paths.

### Background (player-visible symptom)

- Community report: with **Use toilet naked = Never** (or late arousal under **ArousedOnly**), **horny Sims** (including females) could still **autonomously masturbate** on the toilet but remain **clothed on the lower body** — visually wrong for the sex act.
- Original design considered a **player prompt** to undress; product decision replaced it with **automatic lower undress** when masturbation actually starts, plus a dedicated on/off toggle for autonomous toilet masturbation.

### Design — two layers

| Layer | Setting | Governs |
|-------|---------|---------|
| **Entry undress policy** | `UseToiletNaked` (Always / ArousedOnly / Never) | Undress when **entering** the stall — unchanged |
| **Autonomous masturbation** | **`EnableToiletMasturbation`** (new, default ON) | Whether autonomy may roll toilet masturbation at all |
| **Mid-act undress** | (no separate toggle) | **`TryApplyToiletLowerNakedForMasturbation`** when masturbation branch commits — even if entry was dressed |

**Rejected:** `TwoButtonDialog` undress prompt + STBL (`UseToilet.Questions:MasturbateUndressPrompt`) — archived; auto-undress sufficient with dedicated masturbation toggle.

### Root cause (technical)

- Autonomous path: `UseToilet.LoopDel` stage 1→2 sets `goMasturbate` after horny/arousal roll; `StartTeasing` can enter same path.
- `UseToiletNaked = Never` skipped ingress undress; `MakeSimReadyForStandaloneMasturbationVisualParity` handles bot/strap parity only — **not** human lower-body strip.
- No player setting to disable autonomous toilet masturbation independently of global toilet naked policy.

### Fix (code verified — `UseToilet.cs`, `Settings.cs`, `OptionSettingMenuGlobal.cs`)

| Area | File | Behavior |
|------|------|----------|
| New setting | `Oniki/Settings.cs` | **`EnableToiletMasturbation`** — default **`true`**; `Upgrade()` `< 451` + `EnsureReleaseRequiredSettingsPresent()` add-if-missing. |
| UI | `Oniki.UI/OptionSettingMenuGlobal.cs` | **Global → General Gameplay Settings**, immediately **below** `UseToiletNaked`. |
| Run gate | `UseToilet.cs` | `mCanMasturbate` requires `EnableToiletMasturbation` + `!ForceUseGloryHole` + autonomy > Disabled + aroused + wooHoo cooldown (frozen per interaction at `Run()` start). |
| Loop gate | same — `LoopDel` | Stage 1→2 roll only when `GetBool("EnableToiletMasturbation")` live; before `goMasturbate`: **`TryApplyToiletLowerNakedForMasturbation`** — on failure → `ExitReason.Finished`, no clothed masturbation. |
| Teasing gate | same — `StartTeasing` | Same toggle + helper before teasing→masturbate path. |
| Undress helper | same | `LowerNaked \| PantyLess` via `SwitchOutfitHelper`; `mChangeOutfitHelper` restores `prevFlags` on toilet exit (same pattern as entry undress). |

**Gender:** path is **not** female-only — male and female Sims use the same autonomous roll (posture/animation differs).

### Settings interaction matrix (post-fix)

| `UseToiletNaked` | `EnableToiletMasturbation` | Entry | If masturbation starts |
|------------------|----------------------------|-------|-------------------------|
| Always | ON | Lower naked | Helper skips if already naked |
| Never | ON | Dressed | Auto lower undress at sex act |
| ArousedOnly | ON | Naked only if aroused at entry | Auto undress if arousal triggers masturbation later in stall |
| any | OFF | Per `UseToiletNaked` | No masturbation roll / no mid-act undress for masturbation |
| any | ON + `AutonomyLevel` Disabled | Per naked policy | `mCanMasturbate` false — no masturbation (unchanged) |

### Runtime toggle changes (same session, no reload)

| Scenario | Behavior |
|----------|----------|
| Toggle change → **new** toilet interaction | Immediate effect |
| OFF before stage 1→2 roll | Roll blocked (`GetBool` live in `LoopDel`) |
| OFF during stage 2+ (already masturbating) | **Does not** interrupt current animation — finishes current loop (accepted) |
| `mCanMasturbate` | Set once in `Run()` — affects `UseToiletNaked` ArousedOnly **entry** undress for that interaction |

### Unchanged (explicit)

- **`UseToiletNaked`** semantics at stall entry — not replaced by new toggle.
- **`WooHooClothing`** — does not drive toilet lower strip.
- Glory hole paths: `ForceUseGloryHole` still forces `mCanMasturbate = false`; **`EnsureGloryHolePresenterUndressed`** unchanged.
- Shower autonomous masturbation — **out of scope** (optional future wave on `TakeShower`).


### Player support note (non-technical, for release reply)

- **General Gameplay Settings** has a new option **below** “use toilet naked”: control whether Sims can **autonomously masturbate** while on the toilet (default **on** = same as before).
- If toilet naked is set to **Never** but you still allow toilet masturbation, Sims will **automatically** undress the lower body when masturbation actually starts, then dress again when they leave — you will not get a popup asking permission.
- Turn the new toggle **off** to stop autonomous toilet masturbation entirely.

### Diagnostics

- New setting namespace key: `Oniki.KinkyMod.OptionSettings.EnableToiletMasturbation`.
- No prompt STBL (design archived).

---

## Ask to be invited over: forensic logging (Miscellaneous logging)

### Fix

| Area | File | Behavior |
|------|------|----------|
| Acceptance trace | `Oniki.Interactions/AskToBeInvitedOver.cs` | Logs **`[ASK_INVITE_OVER][RESULT_PRE]`**, **`[ACCEPT_OK]`** / **`[ACCEPT_FAIL]`** (reason: null sim/relationship, LTR roll, partner auto-accept, rent scheduler, grouping soft/hard reject), **`[RESULT]`** (Accepted / Awkward / Rejected). |
| Post-social / travel | same | **`[POST_SOCIAL]`** queue snapshot; **`[TRAVEL_PRE]`**, **`[TRAVEL_GROUP_EXIT]`**, branch (**visitor only** / **GoHome+follower** / **fallback**), each **`[TRAVEL_PUSH]`** with `pushed=true/false`, final **`[TRAVEL_OK]`** or **`[TRAVEL_FAIL]`**; **`[TRAVEL_ABORT]`** on null context. |
| Context fields | same | Build, game clock, lot ids/names, LTR/partner/STC, `HoursUntilWork`, grouping state, queue length, current interaction, `ExitReason`. |

### How to enable (test / support builds)

1. **KW → Misc → Logging:** **`Enable global buffer` = ON** and **`Miscellaneous logging` = ON**.
2. Reproduce **2–3** invite attempts (e.g. day + night, different lots).
3. Quit to menu (save or abandon).
4. Send latest **`General_Logging_*.xml`** from the TS3 export folder (search **`[ASK_INVITE_OVER]`**).

### Unchanged (explicit)

- No new settings key or menu toggle — reuses existing **`MiscellaneousLogging`** + **`EnableGlobalBuffer`** gate (`Log.ShouldLogMiscellaneous()`).
- No gameplay or travel logic change in this patch — **diagnostics only**.
- Output channel: **`General_Logging_*.xml`** via `Log.WriteGeneral` (same as other misc traces).

### Diagnostics

- **No new STBL** in this patch.
- Trace prefix for grep: **`[ASK_INVITE_OVER]`**.

---

## Social sync: passive partner crash guard (`KWSocialInteractionPassive`)

### Background (player-visible symptom)

- **Errortrap** (`System.NullReferenceException`) when using **Show cock** / **Ask to show cock** (and potentially other paired KW socials: Tease, Seduce, Flash, etc.).
- Stack pointed at **`KWSocialInteractionPassive.Run`** → EA **`CopyExitReasonToLinkedInteraction`**.
- Reported on **Build 450 v0.9.5**; often seen with **Destrospean** (or similar) mods that patch interaction queue cleanup, but can also occur from cancel/queue races without third-party mods.

### Root cause (technical)

- Paired KW socials link a **master** (`KWSocialInteraction`) and **passive** (`KWSocialInteractionPassive`) instance.
- At end of passive **`Run()`**, KW always called **`CopyExitReasonToLinkedInteraction()`** even when the master was already **removed from queue** or had a **null `InstanceActor`** (stale link after interrupt/cleanup timing).
- EA vanilla **`SocialInteractionB`** already bails out when the linked master is missing from the target queue; KW passive did not.

### Fix

| Area | File | Behavior |
|------|------|----------|
| Linked-master validation | `Oniki.Interactions/KWSocialInteractionPassive.cs` | Before **`CopyExitReasonToLinkedInteraction`**, **`WaitForMasterInteractionToFinish`**, and final sync wait: require **`LinkedInteractionInstance`**, valid **`InstanceActor`**, and master still **queued or transitioning** on **`Target`** (EA-aligned guard). |
| If guard fails | same | Skip tail sync calls; return loop result — **no errortrap**; interaction may still fail silently (same as a broken sync would have looked without the crash). |

### Unchanged (explicit)

- No change to scoring, accept/reject, animations, or **`KWSocialInteraction.RunInternal`** passive creation when sync is healthy.
- **Diagnostics:** no new logging toggles or STBL keys.

### Player support note (non-technical, for release reply)

- If a social “drops” without crash after update, retry without canceling other actions mid-animation; third-party interaction mods can still desync queues — this fix prevents the **crash**, not every mod combo edge case.

---

## WooHoo virginity: single notification at session commit

### Background (player-visible symptom)

- In one WooHoo session with **multiple penetrative stages** (e.g. anal + vaginal-tagged animation, or repeated vaginal stages), both partners could see **duplicate** “lost virginity” notifications / journal **Virgin** stat bumps for the **same** Sim.
- Reported on **gay male couples** (vaginal-tagged + anal stages) and possible on any multi-stage act while **`IsPhysicalVirgin`** was still true mid-session.

### Root cause (technical)

- **`WooHooSkill.ReportAction`** fired **`virginfemale` / `virginmale`** notifications (and **`IncrementStat("Virgin")`**) **per stage** at **`EndStage` → PostWooHoo`**, while **`LostVirginityWithHuman`** (physical virginity) was only committed later in **`PostWooHoo_Effects`** at **`EvaluateOutcomes()`** (session end, including **interrupted** sessions that still ran post-woohoo).
- **`ReportAction`** category came from raw stage XML; runtime vaginal→anal remap in **`PostWooHoo`** did not apply there → **M+M** could notify twice (Vaginal + Anal) before flags were set.

### Fix

| Area | File | Behavior |
|------|------|----------|
| Remove per-stage notify | `Oniki.Gameplay.Skills/WooHooSkill.cs` | Deleted **`virginfemale` / `virginmale`** blocks from **`ReportAction`**. |
| Single source of truth | `Oniki.Utilities/WooHooTools.cs` | New **`NotifyActorOfTargetVirginityLoss`**; called from **`PostWooHoo_Effects`** immediately after **`OnLostVirginity`** + **`LostVirginity*`** flags + journal partner record — **once per target** when physical virginity is actually consumed. |
| Interrupt path | same | Unchanged gate: **`isRealWooHoo`** still drives virginity loss/effects; **`hasBeenInterrupted`** only affects outcome moodlets/feedback text, **not** the virginity commit itself. |

### Unchanged (explicit)

- **STBL keys** (existing only):
  - `Oniki.KinkyMod.woohoo/feedbacks:virginfemale`
  - `Oniki.KinkyMod.woohoo/feedbacks:virginmale`
- Physical virginity rules (**`LostVirginityWithHuman`**, **`OnLostVirginity`**, bloody vagina buff, trait paths) unchanged.
- **`ReportAction`** still records act-type flags, stats, and relationship bits per stage.

### Player support note (non-technical, for release reply)

- If two Sims both start as virgins, you may still see **one message each** (one per Sim) — that is expected. The fix removes **double messages for the same Sim** in a single session.

---

## Autonomy: respect EA Free Will = NO (Patch A)

### Background (player-visible symptom)

- With **Options → General → Free Will = NO**, Sims could still receive **KW-driven** autonomous behavior: meta-autonomy (`CommodityKind.None`), need/moodlet branches, injected scoring, autonomous **computer** use, and similar — while vanilla at the same slider setting is much quieter.
- One reporter “fixed” the issue by toggling **NO → HIGH → NO** on the slider; that only **re-synced** EA `AutonomyRestrictions.SetLevel` and did **not** prove KW already honored OFF before the patch.

### EA slider vs runtime (technical)

| UI (base game) | Saved value | `AutonomyLevel` enum |
|----------------|-------------|----------------------|
| NO | `0` | **`One`** |
| LOW | `1` | `Two` |
| HIGH | `2` | `Three` |

- **`OffForAllSims`** / **`OffForSelectableSims`** exist in EA APIs but are **not** set by the standard options slider (cheats/CLI/other paths).
- At **`One`**, EA still allows some **meta** autonomy in its own loop; KW previously **ignored** `One` in the manager dequeue gate and continued **`FindBestInteraction`** (including `None` when `KWRunAutonomy` is false).

### Root cause (KW)

| Area | Behavior before fix |
|------|---------------------|
| `AutonomyManager.Simulate` dequeue | Skipped only `OffForAllSims` / `OffForSelectableSims` (+ selectable rule); **`One` still queued** → `KWAutonomy.Convert` / `FindBestInteraction`. |
| `KWAutonomy` / inject / requeue | Extra non-vanilla paths (`ForceHighLODAutonomy`, `KWRunAutonomy`, scoring inject, idle requeue, etc.) could surface **local** autonomy even when EA suppressed it for playable Sims at NO. |
| Kinky menu `Settings.GetAutonomyLevel` | At EA `One`, per-Sim KW menu could still be High/Low — separate from this ticket; Patch A stops the **manager engine**, not every direct `Main.Settings.AutonomyLevel` read. |

### Fix (Patch "A")

| Area | File | Behavior |
|------|------|----------|
| EA gate (shared) | `Oniki.Gameplay/AutonomyManager.cs` — `IsEAAutonomyHardOffForActor` | Treat **`AutonomyLevel.One`** as restricted for KW (comment: **Aligned with Free Will = NO in Base Game Slider**). |
| Manager queue | same — `Simulate` dequeue | `autonomyRestricted` now uses **`IsEAAutonomyHardOffForActor(actor)`** (same gate as need-busy override skips). |
| Effect | — | When Free Will = NO, KW **does not** run its autonomy manager cycle for that Sim: no `FindBestInteraction`, no manager-driven `None` fallback, no manager need-busy preempt at that EA level. |

### Unchanged (explicit)

- **LOW / HIGH** slider positions unchanged (`Two` / `Three`).
- Cheat-level **`OffForAllSims`** still honored (was already).
- Per-Sim **Kinky Autonomy** menu values in settings are **not** remapped by this patch (optional future: force `Disabled` when EA is `One`).
- Interactions that bypass the manager queue entirely are out of scope unless reported separately.

### Player support note (non-technical, for release reply)

- **Free Will = NO** means the base game stores **`AutonomyLevel.One`**; KW now **stops its autonomy manager** for that level, matching player expectation of “no mod autonomy” on that slider setting.
- Toggling the slider was a **workaround** to refresh EA state, not proof the mod respected NO earlier.
- If something still looks autonomous at NO, note whether it is a **player-directed** or **scripted** action (not manager queue); send slider screenshot + optional autonomy debug dump.

---

## ScriptError hardening wave (Build 451 test cluster)

### Background (player-visible symptom)

- During **Build 450–451** playtesting (multi-feature 451 wave), many **`ScriptError`** XML dumps appeared across unrelated flows: **world load**, **WooHoo loop**, **whore/brothel stage find**, **bed privacy intrusion**, **paired social** end-loop.
- Symptoms were mostly **`System.NullReferenceException`**; one **`InvalidCastException`** on outfit body-shape refresh involved the **Tom Peeping** NPC (tracked separately — **not** patched in this wave).
- Cluster reviewed from **11** `ScriptError_*.xml` logs (2026-05-29 through 2026-06-08).

### ScriptError cluster (reference)

| Cluster | Count | Stack anchor | Typical actors / context |
|---------|-------|--------------|---------------------------|
| `SimData.Upgrade` | 3 | `Main.Start` → `OnPostEnterWorld(upgrade)` | World load / save upgrade path |
| `WooHooLoop.Run` | 3 | `WooHooLoop.Run` | Mariah Yee, Xavier Yee — mid WooHoo sync/loop |
| `WooHooTools.HasWooHooObjects` | 1 | `WhoreSituation.CreateWooHooInstance` → `FindBestStage` | Stella Yee → Renee Wray (whore/brothel) |
| `Reaction.Play` (null Sim) | 1 | `KWBedSleep.PrivacySituation.OnSimIntruded` → `SimBroadcaster.PlayReaction` | Bed privacy — intruder `CreatedSim` null |
| `KWSocialInteraction.RunInternal` | 1 | `RunInternal` post-loop | Xavier Yee → Mariah Yee — social tail |
| `KWBedSleep.KWLoopHandler` | 1 | Sleep loop handler | **Build 450 only** (single log) |
| `OutfitManager` / `InvalidCastException` | 1 | `WooHooGoTo` → `MakeSimReady` → `RefreshBodyShape` | Harold Peeping — **deferred** (see Out of scope) |

### Root cause (technical)

| Area | Issue |
|------|--------|
| `WooHooTools.HasWooHooObjects` | `LotTools.GetAvailableObjectTypes` returns **`null`** when room has no objects; caller used **`.Count`** without null guard → NRE on whore stage search. |
| `SimData.Upgrade` | `__allSimData` could be null on edge static/init paths; `SkillManager == null` on some `SimDescription` entries during upgrade enumeration; `Household.EverySimDescription()` not null-guarded; no top-level catch — one bad Sim could surface as world-load ScriptError. |
| `Reaction.Play` / `SimBroadcaster.PlayReaction` | Callers (e.g. bed privacy) passed **`simData.CreatedSim`** when instance was null/destroyed; entry points dereferenced `actor` immediately. |
| `WooHooLoop.Run` | Master Sim null/destroyed mid sync; **`Stop()`** invoked when `mWooHooInstance` already null; receiver loop assumed `Master.InteractionQueue` always live; exception handler logged via **`Actor.SimDescription`** after Actor destroyed → secondary NRE / ScriptError. |
| `KWSocialInteraction.RunInternal` | Post-loop **`SocialComponent.UpdateUI`** without null guard; same unsafe catch pattern as WooHooLoop. |
| `WhoreSituation.PostSocialLoop` | **`simData.AddCooldown`** without checking `SimData.Get` result. |

### Fix

| Area | File | Behavior |
|------|------|----------|
| WooHoo object probe | `Oniki.Utilities/WooHooTools.cs` | `HasWooHooObjects`: bail if `stages`, `participants`, or `lot` null; treat `GetAvailableObjectTypes == null` as **no objects** (`return false`). |
| SimData upgrade | `Oniki.Gameplay/SimData.cs` | Re-init `__allSimData` in `OnPostEnterWorld` + `Upgrade`; wrap `Upgrade()` in top-level try/catch; skip Mini/Loaded sims with `SkillManager == null`; iterate `Household.EverySimDescription()` only when non-null. |
| Reaction entry guards | `Oniki.Interactions/Reaction.cs` | `Play(...)`: return false if `actor == null \|\| actor.HasBeenDestroyed`. |
| Broadcaster entry guards | `Oniki.Gameplay/SimBroadcaster.cs` | Both `PlayReaction` overloads: return early if `reactingSim` null/destroyed. |
| WooHoo loop lifecycle | `Oniki.Interactions/WooHooLoop.cs` | Validate `Master` before sync; null-safe `Stop()`; cooldown master only when Sim valid; receiver loop checks `HasBeenDestroyed` + `InteractionQueue != null`; safe catch logging without dereferencing destroyed Actor. |
| Social tail guards | `Oniki.Interactions/KWSocialInteraction.cs` | Null-check `Actor`/`Target.SocialComponent` before `UpdateUI`; safe catch logging. |
| Whore post-social | `Oniki.Situations/WhoreSituation.cs` | `PostSocialLoop`: return if `simData == null` before cooldown/skill path. |

### Out of scope

| Item | Notes |
|------|--------|
| **Tom Peeping / `InvalidCastException`** | `OutfitCategoryMap.ChangeBodyShape` during `MakeSimReady` on NPC `Harold Peeping` (`NPC.IsTomPeeping`). Requires dedicated outfit/CAS bypass — **not** included in this patch set. |

### Unchanged (explicit)

- No new settings toggles or STBL keys.
- No gameplay rule changes (WooHoo scoring, whore tariffs, privacy rules, upgrade migration boundaries).
- Tom Peeping WooHoo/outfit behavior unchanged until a follow-up patch.

### Player support note (non-technical, for release reply)

- This update reduces **script error popups** during load, WooHoo, brothel scenes, bed privacy, and social interactions when Sims or objects are in transient/invalid states. If you still see errors involving **Tom Peeping** and outfit changes, that case is known and planned separately.

### Diagnostics

- **No new STBL** in this patch.
- If ScriptErrors persist, capture the new XML + matching `KWErrorLog_*` / `General_Logging_*` from the same session (with **Enable global buffer** ON if available).

---

## Autonomous "Go Home" for needs: per-audience disable toggles

### Background (player-visible symptom)

- When a Sim is **away from home** on a community or guest residential lot and a **critical base need** cannot be satisfied locally (no fridge, bed, toilet, etc.), KW autonomy can queue **`SimTools.MakeSimGoHome`** as a need-recovery fallback.
- Some players want to **stop** that behavior for specific Sim groups (active Sim only, other household members, or NPCs) without disabling all KW autonomy or unrelated scripted Go Home paths (brothel, school, schedules).

### Fix

| Area | File | Behavior |
|------|------|----------|
| Settings keys | `Oniki/Settings.cs` | Three disable bools (default **`false`** = legacy ON): `DisableAutonomousNeedGoHomeActiveSim`, `DisableAutonomousNeedGoHomeHousehold`, `DisableAutonomousNeedGoHomeNpcs`. |
| Scope helper | same — `IsAutonomousNeedGoHomeDisabledFor(Sim)` | Active Sim → active toggle; other selectable household → household toggle; `!IsSelectable` → NPC toggle. |
| Migration / integrity | same | `Upgrade()` `< 451` + `EnsureReleaseRequiredSettingsPresent()` via `EnsureAutonomousNeedGoHomeSettingsPresent()` (add-if-missing only). |
| UI container | `Oniki.UI/OptionSettingMenuAutonomousGoHomeForNeeds.cs` | New submenu under **Global → General Gameplay Settings**, placed before EP10 entries. |
| Menu wiring | `Oniki.UI/OptionSettingMenuGlobal.cs` | Registered in `OptionSettingMenuGeneralGameplaySettings.Populate()`. |
| Runtime guards | `Oniki.Gameplay/KWAutonomy.cs` | `IsAutonomousNeedGoHomeBlocked()` early return on `TryApplyHungerSemanticFallback`, `TryApplyNeedGoHomeFallback`, and NPC `GoHome` NoCandidate branch. |

**Needs covered (unchanged):** Hunger, Energy, Bladder, VampireThirst, MermaidDermalHydration, Hygiene, Social, Fun — same motive thresholds and exclusions (already home, service Sims, TrickOrTreat/Daycare situations).

### Unchanged (explicit)

- **`EnhancedBasicAutonomy`** recovery paths (need-while-busy, dirty household queue) — not gated by these toggles.
- Scripted Go Home from brothel, high school, schedules, and other direct `MakeSimGoHome` callers.
- NPC Go Home cooldown buff (`GoHome`, 60 min) after manual cancel — unchanged.
- No `Apply()` runtime branch — guards read live settings on each autonomy check.
- Travel RAM: dictionary keys follow standard KW settings clone (Build 449+).

### Player support note (non-technical, for release reply)

- Under **General Gameplay Settings → Autonomous "Go Home" for Needs**, you can disable KW's automatic "go home when a critical need can't be met on this lot" behavior separately for your **active Sim**, **other household members**, and **NPCs**. Defaults keep the old behavior until you turn a toggle on. This does not stop story/schedule-driven travel home.

### Diagnostics

- New menu namespace keys (package STBL): `Oniki.KinkyMod.OptionSettings.MenuAutonomousGoHomeForNeeds.Label`, `Oniki.KinkyMod.OptionSettings.DisableAutonomousNeedGoHomeActiveSim`, `Oniki.KinkyMod.OptionSettings.DisableAutonomousNeedGoHomeHousehold`, `Oniki.KinkyMod.OptionSettings.DisableAutonomousNeedGoHomeNpcs`.
- Optional: with **`AutonomyDebugLogging`** ON, blocked audience should not emit `NeedGoHome` / `make_go_home` traces.

---

## Kraken restructuring: dedicated pregnancy toggle (decoupled from Zoo Lover)

**Tracker:** `TODOs (451)/Kraken Restructuring` — **shipped** (code + UI + migration). STBL import (S3SE).

### Background (player-visible symptom)

- **Kraken attacks** are controlled by **`KrakenEnabled`** (**Global → General Gameplay Settings**, EP10 / Island Paradise).
- **Kraken impregnation** previously depended on **`ZooLover`** (**Global → Zoo Lover**, EP5) — semantically a pets/zoo master switch, far from both Kraken combat settings and the **Pregnancy** menu where fertility/cycle live.
- Players could not enable ocean Kraken pregnancy without enabling Zoo Lover, or disable Kraken conception while leaving Zoo Lover ON for unrelated pet/zoo features.

### Design — two toggles (intentional split)

| Toggle | Menu | Governs |
|--------|------|---------|
| **`KrakenEnabled`** | General Gameplay (EP10) | Whether Kraken attack event can run |
| **`KrakenPregnancyEnabled`** | **Pregnancy → Pregnancy Types** (EP10) | Whether vaginal Kraken phase calls **`Womb.AddSperm`** |

**Rejected UI placements:** under `KrakenEnabled` (mixes event + pregnancy); under **Zoo Lover** (perpetuates wrong coupling).

**Menu tree (Pregnancy Types):**

```
Pregnancy → Pregnancy Types
  … Shemale chance, Droid (EP11), Zoo type / werewolf / fairy (EP5)
  [EP10] Enable Kraken pregnancy     ← NEW (KrakenPregnancyEnabled)
  [EP5]  Zoo pregnancy mermaid chance, Zoo pregnancy chance
```

### Root cause (technical)

- `KrakenAttack.Run()` after vaginal attack phase gated sperm deposit on **`GetBool("ZooLover")`** — legacy Build 450 and earlier (`OKW DLL Extraction` still shows `EnableZooLover` at this callsite).

### Fix (code verified — active `Project` tree)

| Area | File | Behavior |
|------|------|----------|
| Runtime gate | `Sims3.Gameplay.Objects.OnikiStuff/Kraken.cs` | After vaginal phase + creampie effects: `if (KrakenPregnancyEnabled && Womb != null)` → `AddSperm(Target, WooHooFlags.BDSM, false, 5, false)`. **No** runtime read of `ZooLover`. |
| Settings | `Oniki/Settings.cs` | `ResetSettings()` default **`false`**; `Upgrade()` `< 451` + `EnsureReleaseRequiredSettingsPresent()` add-if-missing: seed from **`GetBool("ZooLover")`**. |
| UI | `Oniki.UI/OptionSettingMenuPregnancy.cs` | `OptionSettingEnabledSimple("KrakenPregnancyEnabled")` inside EP10 guard in `OptionSettingMenuPregnancyTypes.Populate()` — inserted **before** EP5 `ZooPregnancyMermaidChance` block. |
| `Apply()` | — | **Not required** — gate read live in `KrakenAttack.Run()`. |

### Behavior matrix (attack vs pregnancy deposit)

| KrakenEnabled | KrakenPregnancyEnabled | ZooLover | Attack | `AddSperm` from Kraken |
|---------------|------------------------|----------|--------|------------------------|
| ON | ON | OFF | Yes | Yes (if womb + KW pregnancy stack OK) |
| ON | OFF | ON | Yes | **No** |
| OFF | any | any | No | No |
| Legacy first load | migrated from ZooLover | — | — | Preserves prior player intent |

### Pregnancy outcome when deposit occurs (unchanged pipeline)

Gate **`KrakenPregnancyEnabled` only controls sperm deposit** — not attack visuals/animations/buffs/creampie moodlets.

| Step | Rule |
|------|------|
| Conception check | Standard `Womb` periodic impregnation after `AddSperm` |
| Father species | **Shark** (`Kraken.SimDescription.mSimFlags`) |
| Pregnancy type | Always **`KinkyPregnancy`** (human Sim baby) — **not** `KinkyPetPregnancy`, regardless of `ZooPregnancyType` Human/Animal/Random |
| Conception trait | Forced **`MermaidHiddenTrait`** |
| Birth | **`ZooPregnancyMermaidChance`** (default 10%, EP5 row below Kraken toggle in same submenu) |

### Runtime dependencies (documented — not gated by new toggle alone)

Conception still requires the usual KW stack: **`EnableKWPregnancy`**, **`EnableFemaleFertility`**, **`EnableMenstrualCycleProgression`**, **`ZooPregnancyType` ≠ Disabled**, fertile Sim / appropriate cycle phase, and completed Kraken vaginal phase with **`KrakenEnabled`** ON.

### Unchanged / out of scope

- **`KrakenEnabled`** location (General Gameplay) — not moved to Pregnancy.
- Attack probability, animations, cooldown, vaginal phase selection — unchanged.
- **`EnableZooLover`** / pet-zoo systems — separate feature; only **one-shot migration** reads `ZooLover` for `KrakenPregnancyEnabled`.
- Follow-up ideas not shipped: unified Kraken submenu; moving `KrakenEnabled` to Pregnancy; altering `KinkyPregnancy` rules for other zoo species.

### Migration / travel RAM

- Dictionary key `KrakenPregnancyEnabled` in `mValues` — follows standard settings clone on sub-world travel (Build 449+).
- Add-if-missing only — never overwrites serialized value after first write.

### Player support note (non-technical, for release reply)

- **Enable Kraken pregnancy** is under **Pregnancy → Pregnancy Types** (Island Paradise). It controls sperm deposit after a Kraken attack — **not** whether the Kraken appears (**Kraken Enabled** in General Gameplay) and **not** Zoo Lover.
- On upgrade, your old **Zoo Lover** setting is copied once into the new toggle so behavior stays familiar.
- Fresh saves default the new toggle **OFF** (same as “needed Zoo Lover OFF” before).
- You still need fertility/pregnancy options enabled for conception to succeed.

### Diagnostics

- Setting namespace key: `Oniki.KinkyMod.OptionSettings.KrakenPregnancyEnabled`.
- Related keys (unchanged): `KrakenEnabled` (General Gameplay), `ZooLover`, `ZooPregnancyMermaidChance`, `ZooPregnancyType`.

---

## Same-sex WooHoo feedback: F+F stage-completed + M+M anal cum-inside (Build 451 wave)

### Background (player-visible symptom)

- **F+F (lesbian couples):** After consensual **unsafe vaginal or anal** WooHoo with **both partners female** and **no real sperm** in the flow (strapon/dildo included; no `AddSperm`), there was no dedicated post-stage dialog — unlike M+F womb/cum-inside feedback. Players expected “stage completed” flavor text, not cum-inside wording.
- **M+M (gay male couples):** After **unsafe anal** with a **male receiver** (including futa/strapon penetrator), there was no **cum-inside** dialog in the existing `CumInsideAnal` family.
- **Shared bug once dialogs shipped:** `PostWooHoo()` runs **per participant × per action** — without dedup, **F+F** could show **two identical** Master-speaker dialogs; **M+M** could show **two bottom POV** cum-inside lines when only one penetrator existed.

### Design split (two families — not duplicates)

| Branch | Couple | Routes | STBL family | Semantic |
|--------|--------|--------|-------------|----------|
| **F+F** | Both female CAS; **no futa v1**; no male | Vaginal + Anal | `WooHoo/Feedback:StageCompleted.{Route}.FF…` | “We finished / that felt good” — **not** cum-inside; strapon-agnostic |
| **M+M** | Male receiver; male or strapon penetrator | Anal only | `WooHoo/Feedback:CumInsideAnal…` | Cum-inside / anal creampie dialog; bottom = receiver |

**Placeholder convention:** `{0}` = speaker (`isTalking`); `{1}` = other participant. M+M cum-inside: MA = bottom (receiver), MB = top (penetrator) via existing `LocalizeCumInsideFeedback`.

### Fix — shared dedup infrastructure

| Area | File | Behavior |
|------|------|----------|
| Consume gate | `Oniki.Gameplay/WooHooInstance.cs` | **`TryConsumeStageCompletedFeedback(channel, simA, simB)`** — key `channel + min:max SimDescId`; dictionary **reset** at each `PostWooHoo()` batch start. |
| F+F channels | `Oniki.Gameplay/LesbianStageCompletedFeedbackTools.cs` | `LesbianStageCompleted.Vaginal` / `.Anal` consume before show. |
| M+M channel | `Oniki.Gameplay/MaleAnalCumInsideFeedbackTools.cs` | `MaleAnalCumInside` consume before show. |

**Dedup outcome (validated 2026-06-06):** one notification per stage per pair — shared fix for both TODO tracks.

### Fix — F+F branch (`LesbianStageCompletedFeedbackTools.cs`)

| Area | Behavior |
|------|----------|
| Hooks | `WooHooTools.PostWooHoo_Vaginal` + `PostWooHoo_Anal` → `TryShowLesbianStageCompletedFeedback(route, …)` |
| Entry gates (all AND) | `!IsRape`; both female; no futa v1; no male; `!isSafe`; **speaker** awake; speaker ≠ victim |
| Speaker (`isTalking`) | **Default / Sis:** `WooHooInstance.Master` among the F+F pair, else `CoinFlip`. **Mommy / Grandma:** fixed parent POV (mother or grandmother always speaks). |
| Resolver priority | Incest (Grandma → Mommy → Sis) → masochist rough on speaker → default `.FF` |
| Localization | Reuses `ConsensualRoughWombFeedbackTools` rough/masochist gates + `LocalizeCumInsideFeedback` helper |

**Key grid:** 2 routes × 4 flavors × 2 (default + `:masochist`) = **16 new STBL keys** (`StageCompleted.Vaginal.FF…`, `StageCompleted.Anal.FF…`).

### Fix — M+M branch (`MaleAnalCumInsideFeedbackTools.cs`)

| Area | Behavior |
|------|----------|
| Hook | `WooHooTools.PostWooHoo_Anal` → `TryShowMaleAnalCumInsideFeedback` when anal non-safe, receiver awake, not victim, male/strapon penetrator |
| Actor gate | **`GetId(actor)==1`** (aligned with male anal creampie donor slot) — prevents double bottom POV |
| Resolver priority | Incest (grandpa → daddy → mother futa → bro) → futa penetrator → masochist rough → default |
| Bottom POV | Receiver when actor is slot 1 (consistent with creampie gameplay) |

**New STBL keys:** **6** (`CumInsideAnal`, `.Futa`, `.Bro`, `.Daddy`, `.Grandpa`, `.MotherFuta`). Reuses existing `Oniki.KinkyMod.WooHoo/Feedback:CumInsideAnal:masochist` (pre-451 rough/womb design).

### Incest / scope (both branches)

Excluded when pair is in `KwHumanRobotGenealogyBuildLineExcludedFromIncestFlavor`; respects `IncestLevel` + active scope settings.

### Unchanged / out of scope

| Item | Notes |
|------|--------|
| **Safe** sex (either route) | No dialog |
| Sleeping speaker/receiver or **victim** context | No dialog |
| **Futa F+F** with sperm | Future `CumInside*` wave — not `StageCompleted` |
| M+F / sperm flows | Existing womb / cum-inside paths unchanged |
| Rape-dedicated keys, step-sibling/aunt/cousin flavors | Not in v1 |
| Strap-on tie-breaker for F+F speaker | Optional future wave; v1 uses Master/CoinFlip + generic copy revision in package |

### Player support note (non-technical, for release reply)

- **Female couples** now get post-WooHoo feedback after unsafe vaginal or anal stages (including strapon scenes) — wording is about the act finishing, not being “filled.”
- **Male couples (anal)** get cum-inside style feedback for the receiving partner.
- If you saw **double popups** for the same stage before 451, that should be fixed — you should see **one** message per stage.
- Incest-flavored lines require incest settings/scopes to allow them; otherwise generic or futa/default paths apply.

### Diagnostics

**F+F — 16 keys** (`Oniki.KinkyMod.WooHoo/Feedback:StageCompleted.*`):

- Vaginal: `…StageCompleted.Vaginal.FF`, `…Vaginal.FF:masochist`, `…Vaginal.FF.Sis`, `…Vaginal.FF.Sis:masochist`, `…Vaginal.FF.Mommy`, `…Vaginal.FF.Mommy:masochist`, `…Vaginal.FF.Grandma`, `…Vaginal.FF.Grandma:masochist`
- Anal: `…StageCompleted.Anal.FF`, `…Anal.FF:masochist`, `…Anal.FF.Sis`, `…Anal.FF.Sis:masochist`, `…Anal.FF.Mommy`, `…Anal.FF.Mommy:masochist`, `…Anal.FF.Grandma`, `…Anal.FF.Grandma:masochist`

**M+M — 6 new keys + 1 reused:**

- `Oniki.KinkyMod.WooHoo/Feedback:CumInsideAnal`, `…CumInsideAnal.Futa`, `…CumInsideAnal.Bro`, `…CumInsideAnal.Daddy`, `…CumInsideAnal.Grandpa`, `…CumInsideAnal.MotherFuta`
- Reused: `Oniki.KinkyMod.WooHoo/Feedback:CumInsideAnal:masochist`

**Code files:** `LesbianStageCompletedFeedbackTools.cs`, `MaleAnalCumInsideFeedbackTools.cs`, `WooHooInstance.cs`, `WooHooTools.cs` (all in `Oniki_KinkyMod` project).

---

## Menstrual cycle robots: separate toggle, chip sync, pregnancy rules

**Tracker:** `TODOs (451)/Menstrual Cycle Robots` — **shipped** (code). STBL label pending (S3SE).

### Background (player-visible symptom)

- Female **EP11 plumbots** with the **Sex Bot chip** could keep an active KW menstrual cycle; **unequipping the chip** did not stop it immediately — `PeriodCycle` moodlets could lapse, but **`mWomb` stayed live** and `Womb.Update()` continued until a CAS-like refresh.
- Community wanted: turn off robot cycles without disabling human cycles; clearer **servobot vs plumbot** pregnancy behavior.

### Root cause (technical)

| Issue | Detail |
|-------|--------|
| Stale womb | Plumbot womb tied to Sex Bot chip (`KwSexBotChipUnlocksRobotWombPath`); `UpdateGender()` disposes womb when chip absent, but **no hook on trait-chip removal** during play. |
| Single human toggle | `EnableMenstrualCycleProgression` was the only cycle master — robots shared or bypassed paths inconsistently in pre-451 drafts. |
| Draft churn | Internal keys `MenstrualCyclePlumbot`, forced `UpdateOvulatory()` when cycle OFF — removed before any public build (no save migration). |

### Design — two independent cycle toggles

| Toggle | Scope | Default |
|--------|-------|---------|
| **`EnableMenstrualCycleProgression`** | **Humans only** (base cycle) | (existing) |
| **`EnableMenstrualCycleRobots`** | **Robots with KW womb** | **`true`** |

**Who qualifies for robot toggle** (`KwQualifiesForRobotMenstrualCycleToggle`):

- **Ambitions servobot** female with **`AllowKinkySimBots`** ON (KW servobot scope).
- **EP11 plumbot** female with **Sex Bot chip** equipped.
- **EP11 plumbot droid** (`SimData.IsDroid` — chip forced in code): robot toggle applies; **not** human cycle.

### Fix (code verified — active `Project` tree)

| Area | File | Behavior |
|------|------|----------|
| Chip sync | `Oniki.Gameplay/SimData.cs` | **`SyncRobotWombWithSexBotChipState()`** each **`Perform()`**: EP11 plumbot, not droid, not servobot-scope path — if Sex Bot chip gone but `mWomb != null` → **`UpdateGender()`** / dispose. |
| Helpers | `Oniki.Utilities/SimTools.cs` | `KwQualifiesForRobotMenstrualCycleToggle`, `KwRobotMenstrualCycleSuppressed`, `KwServobotPregnancyPermanentlyBlocked`. |
| Womb | `Oniki.Gameplay/Womb.cs` | **`IsMenstrualCycleProgressionEnabled`**: sim-aware (human vs robot). **`IsKwRobotMenstrualCycleSuppressedWomb`**: deposit-only when robot cycle OFF. **`AddSperm`**: still deposits in deposit-only mode. **`Update()`**: Luteal park (`mTimeTillNextPhase = MaxValue`) when suppressed; re-`InitializeCycle` when toggle turned back ON from park. Servobot pregnancy cancelled in main path if detected. |
| Settings | `Oniki/Settings.cs` | `EnableMenstrualCycleRobots` — `ResetSettings()` + `Upgrade()` `< 451` + `EnsureReleaseRequiredSettingsPresent()` add-if-missing **`true`**. |
| UI class | `Oniki.UI/OptionSettingEnableMenstrualCycleProgression.cs` | **`OptionSettingEnableMenstrualCycleRobots`** (`OptionSettingEnabled`). |
| Menu | `Oniki.UI/OptionSettingMenuPregnancy.cs` | Main **Pregnancy** menu: human cycle → **robot cycle** → Menstrual Cycle Period → … **`PregnancyPlumbot`** stays in **Pregnancy Types** only. |
| Cleanup | `OptionSettingDroidPregnancy.cs` | Removed draft `OptionSettingPlumbotMenstrualCycle` — no `MenstrualCyclePlumbot` key in sources. |

### Robot cycle OFF — unified deposit-only path

When **`EnableMenstrualCycleRobots = false`** for a qualifying robot:

- **Womb remains** — sperm / creampie deposit OK (`AddSperm` bypasses cycle/fertility gates for deposit).
- Phase parked in **technical Luteal** (`mTimeTillNextPhase = float.MaxValue`) — no progression, no **`Fertile`**, no **new** conceptions.
- **Servobot:** active or pending pregnancy → **cancelled**.
- **Plumbot:** pregnancy **already in progress** before toggle OFF → **continues** (`UpdatePregnancy`); no new conceptions while OFF.

### Pregnancy rules (fixed — validated)

| Sim | Cycle robot ON | Pregnancy |
|-----|----------------|-----------|
| **Servobot F** (in scope) | Full phases + Period if global advanced ON | **Never** (no robot infants in TS3 CAS) |
| **Plumbot F** + chip/droid | Full phases | Only **ovulatory** + **`Fertile`** moodlet + **`PregnancyPlumbot`** ON + global fertility/pregnancy gates — **same window as humans**, no forced conception when cycle OFF |
| **Plumbot**, chip removed | Womb cleared next `Perform` tick | No |

### Shared globals (unchanged — still apply when cycle active)

`EnableFemaleFertility`, `EnableKWPregnancy`, `MenstrualCyclePeriod` (advanced moodlets/PMS), `PregnancyPlumbot` (EP11 conception permission).

**Independence:** human **`EnableMenstrualCycleProgression` OFF** does **not** disable robot cycle when **`EnableMenstrualCycleRobots` ON**.

### UI menu order (Pregnancy root)

```
Pregnancy
  … Enable KW Pregnancy, length
  Enable Menstrual Cycle          (humans)
  Enable Menstrual Cycle for Robots  ← NEW
  Menstrual Cycle Period (advanced)
  Enable Female Fertility, …
  Pregnancy Types → PregnancyPlumbot (EP11), Kraken pregnancy, zoo types, …
```

### Unchanged / removed drafts

- No `Apply()` branch — bool read live in `Womb` / `SimTools`.
- Travel RAM: `EnableMenstrualCycleRobots` in `mValues` follows standard settings clone.
- **`MenstrualCyclePlumbot`** / separate plumbot-only suppress helper — never shipped.


### Player support note (non-technical, for release reply)

- **Pregnancy** menu now has **Enable menstrual cycle for robots**, separate from the human cycle toggle.
- Turn it **off** to stop robot period/fertile phases while still allowing sperm deposit from WooHoo/creampie.
- Removing the **Sex Bot chip** from a plumbot should end the cycle on the next game tick without needing CAS.
- **Servobots** can show a cycle but **cannot become pregnant** by design.
- **Plumbots** can get pregnant only during the normal fertile ovulatory window when pregnancy options allow it — not instantly from every creampie.

### Diagnostics

- New setting namespace key: `Oniki.KinkyMod.OptionSettings.EnableMenstrualCycleRobots`.

---

## Jealousy & cheating overhaul: separate drama, scoring, and journal axes

### Background (player-visible symptom)

- With **Jealousy Level = None**, WooHoo **scoring** often ignored infidelity (`Jealousy.IsActorCheatingWith` returned false), making cheating acceptance **too easy** in the score math — while the **WooHoo skill journal** still incremented **Cheater** stats independently.
- Community reports: **Jealousy None** + high XML cheating multiplier felt inconsistent — easy accept at scoring time but full journal loop afterward; NRaas-style journal tweaks did not map to KW's split systems.
- Players lacked granular control aligned with TS3 base game intuition: low jealousy drama ≠ automatically easy infidelity acceptance ≠ optional journal memory.

### Design — three independent axes

| Axis | Governs | Player control |
|------|---------|----------------|
| **Drama** | NPC reactions, PDA, broadcaster, visibility | `JealousyLevel` (unchanged enum) |
| **Scoring** | “Is this WooHoo infidelity?” + accept difficulty **at request time** | `ScoreInfidelityAsCheating` + `kCheatingDifficultyMultiplier` |
| **Journal** | Post-WooHoo skill book + future ease from past entries | `CheaterJournalReportInfidelity` + `CheaterJournalEasesFutureInfidelity` + separate `MoodletCheater` |

### Root cause (technical)

- `JealousyLevel` was overloaded into WooHoo **scoring** via `IsActorCheatingWith`, not confined to `Jealousy` drama paths.
- `WooHooSkill.ReportAction` incremented `Cheater` on official-partner ≠ WooHoo-partner **without** reading jealousy, scoring settings, or player journal intent.
- `flag2` infidelity branches in `AcceptWooHoo` / `GetWooHooScore` shared the jealousy guard.

### Fix — menu structure (phase 0)

**Global → Jealousy & Cheating** (`OptionSettingMenuJealousyAndCheating.cs`); `JealousyLevel` moved from Global root.

```
Jealousy & Cheating
  1. Jealousy Level
  ── Scoring (at WooHoo accept) ──
  2. Score infidelity in WooHoo scoring
  3. Cheating acceptance difficulty (float)
  ── Journal (after WooHoo) ──
  4. Report infidelity in WooHoo journal
  5. Journal infidelity eases future cheating  [greyed when 4 = OFF]
```

`MoodletCheater` remains under **Global → Moodlet Mechanics** (third lever, not nested here).

| Area | File |
|------|------|
| Container + rows | `Oniki.UI/OptionSettingMenuJealousyAndCheating.cs` |
| Global wiring | `Oniki.UI/OptionSettingMenuGlobal.cs` |
| Settings keys + migration | `Oniki/Settings.cs` |
| Tuning binding (mult float) | `Oniki/KinkyTuningRuntime.cs` |

### Fix — phase 1: decouple jealousy from scoring

| Area | File | Behavior |
|------|------|----------|
| Scoring helper | `Oniki.Utilities/WooHooTools.cs` | **`IsInfidelityForWooHooScoring(actor, target)`** — partner mismatch without `JealousyLevel == None` guard on score paths. |
| Drama isolation | `Oniki.Gameplay/Jealousy.cs` | `IsActorCheatingWith` retained for reactions/PDA only; scoring consumers refactored to helper. |
| Consumers | `WooHooTools.cs` | `AcceptWooHoo`, `GetWooHooScore`, `flag2` branches use scoring helper. |

**Intentional behavior change:** saves with **Jealousy None** + default migration now apply infidelity difficulty in scoring when `ScoreInfidelityAsCheating` is ON — drama at None stays quiet.

### Fix — phase 2: scoring settings

| Setting | Default | When ON | When OFF |
|---------|---------|---------|----------|
| `ScoreInfidelityAsCheating` | `true` | Official partner ≠ WooHoo target counts as infidelity in score math (even Jealousy None) | No infidelity penalty in scoring (independent of jealousy) |
| `KinkyTuning.WooHooTools.kCheatingDifficultyMultiplier` | package **1.5** | Scales `GetCheatingDifficulty` when infidelity active; UI clamp ~0.1–50 | — |

Scoring and journal bools are **independent** — e.g. `ScoreInfidelity = false` + `Report journal = ON` = easy accept now, diary still writes.

### Fix — phase 3: journal settings

| Setting | Default | Behavior |
|---------|---------|----------|
| `CheaterJournalReportInfidelity` | `true` | `ReportAction`: `IncrementStat("Cheater")`, `mFaithful` reset, Cheater trait at 10 uniques, WooHoo skill reputation (**Bitch** if Cheater total > 0; **Faithful** if Cheater == 0 + faithful counter). |
| `CheaterJournalEasesFutureInfidelity` | `true` | When ON **and** report ON, **`CheaterJournalAffectsFutureScoring()`** enables future loops (see below). UI forces OFF/grey when report OFF; code ignores **1=OFF, 2=ON**. |
| `MoodletCheater` | (existing) | Cheater moodlet buff applied on real infidelity **independently** of journal report bool. |

**Future-scoring consumers** (behind `CheaterJournalAffectsFutureScoring`, per-Sim WooHoo skill of actor who cheated):

| Consumer | Legacy effect when ON |
|----------|----------------------|
| `GetCheatingDifficulty` | `-0.25f × GetStatTotalCount("Cheater")`; Cheater trait → 0 difficulty |
| `CanCheat` / `CanCheatWith` | Bypass when Cheater stat > 0 |
| `GetWooHooScore` | `GetStatUniqueCount("Cheater") > 1` score multiplier |

| Area | File |
|------|------|
| Journal guards + split moodlet/stat | `Oniki.Gameplay.Skills/WooHooSkill.cs` |
| Future loop helper | `Oniki.Utilities/WooHooTools.cs` |

### Reference matrices (player tuning)

**Journal only:**

| Report (1) | Eases future (2) | Equivalent | Typical use |
|------------|------------------|------------|-------------|
| OFF | OFF | Journal off | Mod does not track cheating in skill book |
| ON | OFF | Diary without loop | Polyamory / one-off mistake |
| ON | ON | **Legacy 442–450 full** | Default migration |
| OFF | ON | **Invalid** | Treated as OFF/OFF |

**Scoring × journal:**

| ScoreInfidelity | Report | Eases | Effect |
|-----------------|--------|-------|--------|
| OFF | OFF | OFF | Ignore infidelity (score + book) |
| OFF | ON | OFF | Easy accept now; diary yes; no future ease |
| OFF | ON | ON | Easy now + stat loop (power user) |
| ON | ON | ON | Full legacy + Jealousy None scoring fix |

### Fix — debug utility (phase 3f)

| Area | File | Behavior |
|------|------|----------|
| Pie menu | `Oniki.Interactions.Debug/Sim_ClearJournalFromCheatings.cs` | Debug only: **Clear Journal From Cheatings** on target Sim. |
| Clear API | `WooHooSkill.ClearCheaterJournalRecord()` | `ResetStat("Cheater")`, `mFaithful = 0`, remove Cheater trait, remove active Cheater moodlet. |

**Does not touch:** `Corrupted` skill stat, EA betrayal/reputation, `NotifyCheatingWooHooStory`, other WooHoo stats (Vaginal, Public, …).

### WooHoo feedback — `shouldstop` vs `Interruptus` (documentation)

Two **different** feedback paths often confused in support:

| Namespace key | Phase | Trigger (summary) |
|---------------|-------|-------------------|
| `Oniki.KinkyMod.woohoo/feedbacks:shouldstop` | **During** `WooHooLoop` | Natural stop + next stage more intense than current + `StageAccepted` false because `difficulty > score` (includes infidelity when `ScoreInfidelityAsCheating` ON). Dialog then `End(Finished, false)`. |
| `Oniki.KinkyMod.WooHoo:Interruptus` | **Post** `EvaluateOutcome` | `ExitReason != Finished` and (caught **or** sequence shorter than min duration). Not tied to scoring infidelity setting. |

**UX note:** `shouldstop` copy sounds like guilt/cheating but the code branch covers **any** stage-escalation reject where score loses to difficulty (LTR, exhibition, infidelity, journal loop, etc.) — not a dedicated cheating-only message.

### Migration / persistence

| Hook | Behavior |
|------|----------|
| `Upgrade()` `< 451` | Add-if-missing: all three bools `true`; multiplier via existing Kinky Tuning capture. |
| `ResetSettings()` | Same defaults (`true` / package 1.5). |
| Travel RAM | Dictionary bools + `KinkyTuning.WooHooTools.kCheatingDifficultyMultiplier` follow standard settings clone. |

**Historical stats:** turning report OFF stops **new** Cheater writes; existing journal data **not** wiped. Turning eases OFF stops future loops; past stats remain until debug clear.

### Unchanged (explicit)

- `JealousyLevel` drama semantics (reactions, broadcaster, PDA) — unchanged.
- `CheaterJournalMode` enum — removed from design, not shipped.
- EA vanilla betrayal systems outside KW WooHoo skill journal.

### Player support note (non-technical, for release reply)

- **Jealousy & Cheating** splits three ideas: jealousy **reactions**, **how hard** Sims accept cheating WooHoo, and whether the **skill journal** tracks cheating / makes repeats easier. Defaults match old full behavior for upgraded saves.
- **Jealousy None** no longer automatically makes cheating easy in scoring — use **Score infidelity** and **acceptance difficulty** instead.
- **Cheater moodlet** is still its own toggle under Moodlet Mechanics.
- Debug mode: **Clear Journal From Cheatings** on a Sim wipes KW cheating journal stats (not Corrupted or EA betrayal).

### Diagnostics

- Menu/setting namespace keys: `Oniki.KinkyMod.OptionSettings.MenuJealousyAndCheating.Label`, `Oniki.KinkyMod.OptionSettings.ScoreInfidelityAsCheating`, `Oniki.KinkyMod.OptionSettings.CheaterJournalReportInfidelity`, `Oniki.KinkyMod.OptionSettings.CheaterJournalEasesFutureInfidelity`, `Oniki.KinkyMod.OptionSettings.KinkyTuning.WooHooTools.kCheatingDifficultyMultiplier`, `Oniki.KinkyMod.Debug.ClearJournalFromCheatings`.
- Feedback keys (support reference only): `Oniki.KinkyMod.woohoo/feedbacks:shouldstop`, `Oniki.KinkyMod.WooHoo:Interruptus`.
- XML default reference: `ONIKI_KinkySettings.package` → `WooHooTools.kCheatingDifficultyMultiplier` = 1.5.

---

## Kinky Tuning (Advanced): per-save XML tunable overrides

**Tracker:** `TODOs (451)/Kinky Tuning` — **shipped** (code + package STBL pass). Cross-ref: **`kCheatingDifficultyMultiplier`** also exposed under **Jealousy & Cheating** (same binding).

### Background (player-visible symptom)

- Balancing values for KW live primarily in **`ONIKI_KinkySettings.package`** XML tunables (`[Tunable]` static fields). Players and support had no in-game way to inspect or override those values **per save** without editing packages.
- Post-WooHoo LTR gains per stage, exhibition multipliers, whore tariffs, and cheating difficulty were among the most requested tuning levers.

### Root cause (technical)

- Runtime reads static `[Tunable]` fields loaded from the tuning package; `Settings.mValues` had no systematic bridge beyond a few hardcoded Misc dictionary sliders (e.g. **`KWWooHooIntensity`**).

### Design — per-save shadow overrides (not package rewrite)

| Layer | Source |
|-------|--------|
| **Package/XML default** | `[Tunable]` statics from `ONIKI_KinkySettings.package` — captured once into RAM shadow (`CapturePackageDefaultsIfNeeded`) |
| **Save override** | `Settings.mValues` keys `KinkyTuning.<Class>.<Field>` (or array element suffix) |
| **Effective runtime** | On `Apply()` / injector path: override if present (clamped), else shadow default → reflection write to static fields |

**Menu path:** **Global → Misc** (not root) → **Kinky Tuning (Advanced)** — penultimate Misc row, before **Interactions Tuning**.

### Fix (code verified — `KinkyTuningRuntime.cs`)

| Area | File | Behavior |
|------|------|----------|
| Runtime bridge | `Oniki/KinkyTuningRuntime.cs` | **New.** `sBindings[]` (**46** registered fields), binding kinds: `Float`, `Bool`, `Int`, `FloatArrayElement`, `IntArrayElement`. `ApplyFromSettings()` reflection write. |
| Settings | `Oniki/Settings.cs` | `Apply()` → `KinkyTuningRuntime.ApplyFromSettings`; `EnsureKinkyTuningSettingsPresent()` on `< 451` + integrity helper — **capture shadow only**, no `KinkyTuning.*` keys in `ResetSettings()` until player edits. |
| Injector | `Oniki.Interactions/InteractionInjector.cs` | After `ApplySavedTunings()`, also applies Kinky Tuning overrides. |
| UI shell | `Oniki.UI/OptionSettingMenuKinkyTuning.cs` | Root + **5** submenus (`OptionMenu`). |
| Row widgets | `Oniki.UI/OptionSettingKinkyTuningValue.cs` | Float/int EditPrompt dialogs + one bool toggle (`kOnlyGainPointsAroundOtherSims`, no EditPrompt). Labels via `KinkyTuningUiLabels.ResolveRowLabel` / `ResolveEditPromptText`. |
| Misc wiring | `Oniki.UI/OptionSettingMenuMisc.cs` | `AddItem(new OptionSettingMenuKinkyTuning())` before Interactions Tuning. |

### Submenus and binding scope

| Submenu | Modules / examples |
|---------|-------------------|
| **Skills** | `KinkySkill.kIncreaseScale`; `ExhibitionTools` intensity / online difficulty / bool |
| **Relationships** | `WooHooSequence.kWooHooLTRgain`; `Seduce.LTRgainFromAttraction`; `WooHooTools.kPostWooHooLTRgain*` (7 stage fields) |
| **Desire** | `WooHooTools` — `kIntensity`, `kSoloIntensity`, `kWTFMultiplier`, `kWooHooScoreTraitBonus`, `kCheatingDifficultyMultiplier` |
| **Exhibition** | `Scoring` — reaction + exhibition pool/beach/towel/night/outside/community/home/alone factors (**9** floats) |
| **Economy** | `WhoreSituation` duration/customers; `WooHooSkill` base tariffs (5 act types), whore title skill levels / customers / earnings (4 tiers × 3 arrays) |

### LTR tuning — two separate layers (do not conflate)

| Tunable | When it applies |
|---------|-----------------|
| **`WooHooSequence.kWooHooLTRgain`** | **Once** when WooHoo request is **accepted** (`PostSocialLoop`), before stages |
| **`WooHooTools.kPostWooHooLTRgain*`** | Per **stage** on successful `PostWooHoo` completion (Teasing / Handjob / Oral / Vaginal base+scale / Anal base+scale); requires Misc **`KWWooHooLTRgain`** ON |

**Build 451 behavior change:** all partner types (human, pet/zoo, etc.) use the same PostWooHoo LTR tunables — removed legacy fixed **`100f`** on non-human vaginal/anal only. Professional vaginal/anal uses `*Base` when `outcome >= 1`.

### EditPrompt UX (float/int rows)

- Prompt = row **`…<Field>.EditPrompt`** + blank line + shared suffix from **`OptionSettings.KinkyTuning.EditPrompt.PackageDefault`** (`{0.String}` = shadow package value, **not** the saved override).
- Missing row STBL → XML field name fallback (`GetXmlFieldDisplayFallback`); missing EditPrompt → generic English fallback in code.
- Operational STBL manifest: `STBLs/_kinky_editprompt_work/KINKY_TUNING_EDITPROMPT_MANIFEST.txt`.

### Unchanged / limits (explicit)

- Does **not** write to **`ONIKI_KinkySettings.package`** on disk.
- Does **not** replace Misc **`KWWooHooIntensity`** (player slider — separate from XML **`WooHooTools.kIntensity`**).
- Only **`sBindings`** registry exposed — not every mod `[Tunable]`.
- Travel RAM: overrides in `mValues` follow whole-settings clone (Build 449+ model).

### Player support note (non-technical, for release reply)

- **Misc → Kinky Tuning (Advanced)** lets you change advanced balance numbers from the mod's tuning XML **for this save only**. Your installed package defaults apply until you change a value. The edit box shows the original package default for reference.
- This is **not** the same as the WooHoo intensity slider in Misc — that remains the simple player-facing control.
- Cheating acceptance difficulty can be tuned here **or** under **Jealousy & Cheating** (same underlying value).

### Diagnostics

**Menu namespace keys:**

- `Oniki.KinkyMod.OptionSettings.MenuKinkyTuning.Label`
- `Oniki.KinkyMod.OptionSettings.MenuKinkyTuning.Skills.Label`, `…Relationships.Label`, `…Desire.Label`, `…Exhibition.Label`, `…Economy.Label`

**Shared EditPrompt suffix:**

- `Oniki.KinkyMod.OptionSettings.KinkyTuning.EditPrompt.PackageDefault`

**Row labels / EditPrompts:** `Oniki.KinkyMod.OptionSettings.KinkyTuning.<Class>.<Field>` and `…<Field>.EditPrompt` per binding.

**PostWooHoo LTR EditPrompt family (Relationships — 7 keys):**

- `Oniki.KinkyMod.OptionSettings.KinkyTuning.WooHooTools.kPostWooHooLTRgainTeasing.EditPrompt`
- `…kPostWooHooLTRgainHandjob.EditPrompt`
- `…kPostWooHooLTRgainOraljob.EditPrompt`
- `…kPostWooHooLTRgainVaginalBase.EditPrompt`
- `…kPostWooHooLTRgainVaginalOutcomeScale.EditPrompt`
- `…kPostWooHooLTRgainAnalBase.EditPrompt`
- `…kPostWooHooLTRgainAnalOutcomeScale.EditPrompt`

**Save key examples:** `KinkyTuning.WooHooSequence.kWooHooLTRgain`, `KinkyTuning.WooHooTools.kCheatingDifficultyMultiplier`, `KinkyTuning.WooHooSkill.kBaseWhoringTarif.Vaginal`.

---

## Brothel UX redesign (guided hire, schedule gates, household fixes)

### Background (player-visible symptoms)

- **Hire → schedule dead-end:** Recruiting a whore/janitor/bouncer only added the Sim to the global employee list; **lot assignment** and **work schedule** were separate manual steps. Opening **Work schedule** or the weekly planning grid with no `LotId` produced **empty menus that closed silently** — reported as “menu opens and closes immediately” (RU community).
- **“Only one whore works” confusion:** The **manager/owner** bypasses employee schedule gates and can solicit on the brothel lot immediately; hired household members need **assign + WooHoo shift + open hours**. Setting **`CallgirlServiceMaxWhores`** affects only the **phone callgirl NPC pool**, not brothel staff — raising it does not increase brothel employees.
- **Household house arrest:** Playable household whores on a **dedicated brothel lot** (not home) could not leave for errands/hospital when the brothel was **closed** or outside **Free time**; KW repeatedly queued **Go Home** every ~10 sim minutes, interrupting rabbit holes and player-directed trips. Legacy intent was anti-NPC drift; effect was game-breaking for selectable Sims.

### Root cause (technical)

| Area | Issue |
|------|--------|
| Hire pipeline | `RecruitWhore` / `RecruitEmployee` → `AddEmployee` only; `AssignEmployee` / `brothel.AddEmployee` optional and buried in profile UI. |
| Schedule gates | Inconsistent: whores could open `MenuSchedule` without assignment; `ScheduleInterval.Populate()` stayed empty → zero-row `OptionMenu`. |
| Planning grid | `PlanningDay` iterates `brothel.Employees` only — empty when nobody assigned to that lot. |
| House arrest | `BrothelManager.Update` (10 sim min): closed brothel → `StopWorking` all; `StopWorking` → `MakeSimGoHome` for everyone; `Schedule.Proceed` / `GoToWork` during work shift even when closed. |
| Manager vs employee | `WhoreSituation.SolicitCustomer`: `IsManager` bypass; employees require `GetScheduleAction == WooHoo` + presence on assigned lot. |

### Fix — hire, assign, and schedule gates (Slice A)

| Area | File | Behavior |
|------|------|----------|
| Schedule guards | `Oniki.Gameplay/BrothelManager.cs` | `CanConfigureSchedule`, `NotifyScheduleBlockedReason`, `NotifyBrothelScheduleNeedsEmployees()`; guards on `MenuPlanningWeek`, `PlanningDay`, `ScheduleInterval`, `MenuSchedule`, `ScheduleDay` (OnSelected + Populate). Empty planning/interval → explicit notification, not silent close. |
| Post-hire flow | same | `OnEmployeeHired` / `TryAutoAssignAfterHire` after successful `AddEmployee`. |
| **1 brothel** | same | Silent `AssignEmployee` + manager notification (`Brothel:EmployeeAutoAssigned`). |
| **2+ brothels** | same | Modal `MenuAssignment(postHireRequired: true)` — brothel list `[B]…` only, **not** map `LotTools.ChooseLot`. OK → `Brothel:EmployeeAssigned` to manager; Cancel → `CancelPostHireEmployee` (removes profile, fires only `Whoring` if present) + `Brothel:HireAssignmentCancelled`. **No suspended `LotId=0` state** after cancel. |
| Menu cleanup | same | Removed `***` separator rows from `MainMenu`, `MenuReports`, `ReportDay`, `MenuBrothels`. |
| Recruit gate | `Oniki.Interactions/RecruitWhore.cs`, `RecruitEmployee.cs` | If `mBrothels.Count == 0` → interaction greyed + tooltip `Brothel:RecruitNeedsRegisteredLot` via `TrySetRecruitRequiresBrothelTooltip`. |

### Fix — unassigned employees and menu labels (Slice D-lite)

| Area | File | Behavior |
|------|------|----------|
| Unassigned container | `BrothelManager.cs` | Root **Employees (All Brothels)** → **Lot Unassigned Employees** (`LotId == 0`); per-Sim `MenuSim` (Profile / Assign / Schedule). Unassigned Sims still appear in per-role lists (known overlap — backlog). |
| Label split | same | Root: `Brothel:MenuEmployeesAllBrothels`; per-brothel manage: `Brothel:MenuEmployeesThisBrothel`. Legacy `Brothel:MenuEmployees` no longer referenced in code. |

### Fix — household house arrest partial (Slice B)

| Area | File | Behavior |
|------|------|----------|
| Closed brothel tick | `BrothelManager.cs` | `Update`: when closed → `StopWorking` **NPCs only** (`!CreatedSim.IsSelectable`). |
| Stop working | same | `StopWorking`: `MakeSimGoHome` + GoHome motive **NPCs only**; selectable get work cleanup without forced go-home queue. |
| Open helper | same | `IsEmployeeBrothelOpen(sim)` — central gate for schedule routing. |
| Schedule routing | `Oniki.Gameplay/Schedule.cs` | `Proceed` / `GoToWork`: skip when employee has **work shift** but brothel **closed** (selectable + NPC). Uninstantiated Sim: `GoToWork` only if brothel open. |
| Closed tick sync | `BrothelManager.cs` | When closed: no `Proceed()`; `SyncScheduleActionState()` for assigned employees — prevents spurious shift notifications on reopen. |

### Fix — in-game brothel settings (Slice D-lite + UX-feedback)

New submenu **Brothel → Settings** (before End Business). Keys in `Settings.mValues`; migration `< 451` via `EnsureBrothelGoToWorkSettingsPresent()`.

| Setting key | Default | Behavior |
|-------------|---------|----------|
| `BrothelSelectableAutoTravelToLot` | `false` | Household selectable employees: `Schedule.GoToWork` queues travel to assigned lot only when ON. |
| `BrothelGoToLotCooldownMinutes` | `20` | Float clamp 1–180; `SimData` cooldown `Brothel.GoToWork.Selectable` after manual cancel. NPC retry **2 min** hardcoded (`Brothel.GoToWork.NPC`). |
| `BrothelNpcPrioritizeEaWorkOrSchool` | `true` | When ON: NPC employees defer brothel `Proceed`/`GoToWork` during EA fixed-hour school/work or when queue has school/work rabbit hole / carpool. **Defensive brake only** — no active handoff at hour boundary. Selectable **not** affected. |
| `BrothelEmployeeShiftNotificationAudience` | `Household` | Rotation: Household → NPCs → Everyone → Disabled. Controls shift boundary TNS (below). |

**Menu order:** Go To Lot toggle + cooldown adjacent; NPC EA priority **penultimate** (above shift notifications).

### Fix — shift Work/Free Time notifications (Slice UX-feedback)

| Area | File | Behavior |
|------|------|----------|
| Boundary detect | `Schedule.cs` | `TryNotifyEmployeeShiftChange` at start of `Proceed()`: notify only on **block** transition (work = any `ActionNames` ≠ `None`; free = `None`). Contiguous WooHoo→Dance = one work block. |
| Prerequisites | same | Assigned `LotId`; `IsEmployeeBrothelOpen`; Sim instantiated; audience not Disabled; `Settings.ShouldNotifyBrothelEmployeeShift(sim)`. |
| Independence | same | Fires **before** `GoToWork` / at-work checks — works when selectable is off-lot (including Go To Lot OFF). |
| Closed brothel | `BrothelManager.cs` | **No TNS** when closed (aligned with `Brothel:Closed` picker rows). |
| Time label | `Schedule.cs` | `{1.String}` via `SimClockUtils.GetTimeAsText` (game 12/24h + locale); removed hardcoded English AM/PM. |
| Audience enum | `Oniki/AnnounceLevels.cs` | `BrothelEmployeeShiftNotificationAudience`. |

### Fix — employee profile shortcuts and planning UX

| Area | File | Behavior |
|------|------|----------|
| Pie — manager self | `Oniki.Interactions/Brothel_OpenMenu.cs` | `Brothel_OpenMenu:InteractionName` → `StartManage()` → root `MainMenu`. |
| Pie — manager → employee | same + `BrothelManager.OpenEmployeeMenu` | Label `Brothel:MenuSim` with `{0.SimName}` → direct `MenuSim` (Profile / Assignment / Occupation Conflicts / Schedule), not root menu. |
| Terrain shortcut | `Oniki.Interactions/Terrain_OpenBrothelMenu.cs` | Active manager clicks routable terrain on registered brothel lot → immediate `StartManage()` (no walk). No water/fountain; lot must be in `mBrothels`. |
| MenuSim title | `BrothelManager.cs` | `UITools.Localize("Brothel:MenuSim", sim)` — requires `{0.SimName}` in STBL. |
| Planning grid | same — `PlanningDay` | Local `GetColumnSize`: employee column **150 px**, hour columns **92 px** (default OptionMenu 325/75). |

### Fix — occupation conflicts (Slice conflict UX)

| Area | File | Behavior |
|------|------|----------|
| Conflict scan | `Oniki.Gameplay/BrothelEmployeeScheduleConflict.cs` | **New.** EA school + fixed-hour careers (excludes Whoring, skill-based, open hours). Teen high school: `SchoolLocation` fallback when `CareerOfferWorkHours` empty. |
| MenuSim row | `BrothelManager.cs` | `Brothel:OccupationConflicts` with conflict count; **0** → `Brothel:OccupationConflicts.None` dialog; **≥1** → 3-column picker (Name / Days / Hours) → row click → informational `Brothel:OccupationConflicts.List` (no gameplay action). |
| Localized days/hours | `BrothelEmployeeScheduleConflict.cs` | `FormatLocalizedWorkDays` via `DaysOfWeekToWork` + `SimClockUtils.GetDayAsTextAbrev` (e.g. `Mon - Fri`, not EA `MTWRF`); hours via `SimClockUtils.GetText`. |
| Sim names in STBL | `BrothelManager.cs` | `UITools.Localize(key, sim)` → `{0.SimName}` token (not `{0.String}` on `SimDescription`). |

### Menu structure (Build 451 reference)

**Root `MainMenu`** (phone / PC / pie / terrain):

```
Employees (All Brothels)
  → Lot Unassigned Employees [LotId == 0 only]
  → Whores / Janitors / Bouncers [all mEmployees by role]
Brothels → Reports → Settings → End Business
```

**Per-brothel `ManageBrothel`:** Employees (This Brothel) → role lists → schedules/tariffs (unchanged).

**Employee `MenuSim`:** Profile → Assignment → Occupation Conflicts → Schedule.

### Unchanged (explicit)

- **`BrothelSituation`** client AI, weekly economy, police raid — not rewritten.
- **`Callgirl` service** pool logic and `CallgirlServiceMaxWhores` scope — unchanged (doc clarification only; tooltip backlog).
- **Manager/owner** still bypasses employee schedule for on-lot solicit (legacy design).
- **Phase 2** setup wizard, employee setup badges, onboarding text refresh for `Brothel:CreateManagerQuestion` / `Brothel:Created` — design documented, not shipped.
- **Phase 2b** employment mode dialog (Convert / Create Career / Free Lance) — not shipped.
- Unassigned employees still appear in per-role root lists (design debt).


### Player support note (non-technical, for release reply)

- Brothel staff need three steps: **hire** → **assign to a brothel lot** → **set work schedule** (WooHoo/Maintenance/etc. during open hours). The manager can work immediately; employees only work during their scheduled shifts on the assigned lot.
- **`Callgirl Service Max Whores`** does **not** limit brothel employees — only phone callgirls.
- If household whores were trapped at home, this update reduces forced Go Home when the brothel is closed; use **Brothel → Settings** to control household auto-travel to the lot (default off) and shift notifications.
- Recruiting before registering any brothel lot is now blocked with an explanation.

### Diagnostics

- Brothel settings keys: `BrothelSelectableAutoTravelToLot`, `BrothelGoToLotCooldownMinutes`, `BrothelNpcPrioritizeEaWorkOrSchool`, `BrothelEmployeeShiftNotificationAudience` (in `Settings.mValues`, not `OptionSettings.*` prefix).
- New/updated menu and feedback namespace keys (package STBL): `Oniki.KinkyMod.Brothel:ConfirmAssignment`, `Brothel:EmployeeAutoAssigned`, `Brothel:EmployeeAssigned`, `Brothel:HireAssignmentCancelled`, `Brothel:ScheduleNeedsAssignedEmployees`, `Brothel:RecruitNeedsRegisteredLot`, `Brothel:MenuUnassignedEmployees`, `Brothel:MenuEmployeesAllBrothels`, `Brothel:MenuEmployeesThisBrothel`, `Brothel:MenuSim`, `Brothel_OpenMenu:InteractionName`, `Brothel:MenuSettings`, `Brothel.Settings:SelectableAutoTravelToLot`, `Brothel.Settings:SelectableGoToLotCooldown`, `Brothel.Settings:EnterCooldownMinutes`, `Brothel.Settings:EmployeeShiftNotifications` (+ `.Household` / `.NPCs` / `.Everyone` / `.Disabled`), `Brothel:ShiftEnteredWork`, `Brothel:ShiftEnteredFreeTime`, `Brothel.Settings:NpcPrioritizeEaWorkOrSchool`, `Brothel:OccupationConflicts` (+ `.Title`, `.None`, `.List`, `.ColumnName`, `.ColumnDays`, `.ColumnHours`).
- Legacy keys still referenced: `Oniki.KinkyMod.Brothel/:EmployeeNeedsAssignment`, `Brothel:CreateManagerQuestion`, `Brothel:Created` (onboarding text refresh pending).
